﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Linq.Expressions;
using System.Text.Json;
using Volo.Abp;
using Volo.Abp.DependencyInjection;

namespace Joy.Erp.BasicArchives.Core.Filters
{
    public class FilterConverter : IFilterConverter, ISingletonDependency
    {
        public Expression<Func<TEntity, bool>> Convert<TEntity>(FilterGroupDto group)
        {
            Check.NotNull(group, nameof(group));

            var param = Expression.Parameter(typeof(TEntity), "entity");
            return Expression.Lambda<Func<TEntity, bool>>(ConvertToExpressionBody(group, param), param);
        }

        public Expression<Func<TEntity, bool>> Convert<TEntity>(FilterRuleDto rule)
        {
            Check.NotNull(rule, nameof(rule));

            var param = Expression.Parameter(typeof(TEntity), "entity");
            return Expression.Lambda<Func<TEntity, bool>>(ConvertToExpressionBody(rule, param), param);
        }

        private Expression ConvertToExpressionBody(FilterGroupDto group, ParameterExpression param)
        {
            var body = Expression.Constant(true) as Expression;
            if (group.Operate == FilterGroupOperate.And)
            {
                foreach (var innerGroup in group.Groups)
                {
                    body = Expression.And(body, ConvertToExpressionBody(innerGroup, param));
                }

                foreach (var innerRule in group.Rules)
                {
                    body = Expression.And(body, ConvertToExpressionBody(innerRule, param));
                }
                return body;
            }
            else if (group.Operate == FilterGroupOperate.OrElse)
            {
                body = Expression.Constant(false) as Expression;
                foreach (var innerGroup in group.Groups)
                {
                    body = Expression.OrElse(body, ConvertToExpressionBody(innerGroup, param));
                }

                foreach (var innerRule in group.Rules)
                {
                    body = Expression.OrElse(body, ConvertToExpressionBody(innerRule, param));
                }
            }
            return body;

        }

        private Expression ConvertToExpressionBody(FilterRuleDto rule, ParameterExpression param)
        {
            string[] propertyNames = rule.Property.Split('.');

            var propertyAccess = param as Expression;
            var propertyType = param.Type;
            for (var index = 0; index < propertyNames.Length; index++)
            {
                string propertyName = propertyNames[index];
                PropertyInfo property = propertyType.GetProperty(propertyName);
                if (property == null)
                {
                    throw new BusinessException("BasicArchives:002").WithData("PropertyName", rule.Property);
                }

                propertyType = property.PropertyType;
                propertyAccess = Expression.MakeMemberAccess(propertyAccess, property);
            }

            var expression = Expression.Lambda(propertyAccess, param);
            if (expression == null) return Expression.Constant(true);

            if (rule.Operate == FilterRuleOperate.In)
            {
                return In(expression.Body, rule.Value);
            }

            object value = null;
            try
            {
                value = rule.Value.CastTo(expression.Body.Type);
            }
            catch (Exception)
            {
                throw new BusinessException("BasicArchives:004").WithData("Value", rule.Value).WithData("TypeName", expression.Body.Type.Name);
            }
            Expression constant = Expression.Constant(value, expression.Body.Type);

            switch (rule.Operate)
            {
                case FilterRuleOperate.Equal: 
                    return Expression.Equal(expression.Body, constant);
                case FilterRuleOperate.NotEqual:
                    return Expression.NotEqual(expression.Body, constant);
                case FilterRuleOperate.LessThan:
                    return Expression.LessThan(expression.Body, constant);
                case FilterRuleOperate.LessThanOrEqual:
                    return Expression.LessThanOrEqual(expression.Body, constant);
                case FilterRuleOperate.GreaterThan:
                    return Expression.GreaterThan(expression.Body, constant);
                case FilterRuleOperate.GreaterThanOrEqual:
                    return Expression.GreaterThanOrEqual(expression.Body, constant);
                case FilterRuleOperate.StartsWith:
                    return StartsWith(expression.Body, constant);
                case FilterRuleOperate.EndsWith:
                    return EndsWith(expression.Body, constant);
                default:
                    return Expression.Constant(true);
            }

        }

        private static Expression StartsWith(Expression left, Expression right)
        {
            var methodInfo = left.Type.GetMethod("StartsWith", new[] { typeof(string) });
            if (methodInfo == null) throw new BusinessException("BasicArchives:003").WithData("TypeName", left.Type.Name).WithData("FunName", "StartsWith");
            return Expression.Call(left, methodInfo, right);
        }

        private static Expression EndsWith(Expression left, Expression right)
        {
            var methodInfo = left.Type.GetMethod("EndsWith", new[] { typeof(string) });
            if (methodInfo == null) throw new BusinessException("BasicArchives:003").WithData("TypeName", left.Type.Name).WithData("FunName", "EndsWith");
            return Expression.Call(left, methodInfo, right);
        }

        private Expression In(Expression left, object rawValue)
        {
            var elementType = left.Type;
            var list = BuildConstantList(rawValue, elementType);

            if (((ICollection)list).Count == 0)
            {
                return Expression.Constant(false);
            }

            var listType = list.GetType();
            var containsMethod = listType.GetMethod("Contains", new[] { elementType });
            if (containsMethod == null)
            {
                throw new BusinessException("BasicArchives:003").WithData("TypeName", listType.Name).WithData("FunName", "Contains");
            }

            var constant = Expression.Constant(list, listType);
            return Expression.Call(constant, containsMethod, left);
        }

        private static object BuildConstantList(object rawValue, Type elementType)
        {
            var listType = typeof(List<>).MakeGenericType(elementType);
            var list = (IList)Activator.CreateInstance(listType);

            if (rawValue == null)
            {
                return list;
            }

            if (rawValue is string str)
            {
                list.Add(str.CastTo(elementType));
                return list;
            }

            if (rawValue is JsonElement jsonElement)
            {
                if (jsonElement.ValueKind != JsonValueKind.Array)
                {
                    list.Add(jsonElement.CastTo(elementType));
                    return list;
                }

                foreach (var item in jsonElement.EnumerateArray())
                {
                    if (item.ValueKind == JsonValueKind.Null)
                    {
                        list.Add(null);
                    }
                    else
                    {
                        list.Add(JsonSerializer.Deserialize(item.GetRawText(), elementType));
                    }
                }
                return list;
            }

            if (rawValue is IEnumerable enumerable)
            {
                foreach (var item in enumerable)
                {
                    list.Add(item.CastTo(elementType));
                }
                return list;
            }

            list.Add(rawValue.CastTo(elementType));
            return list;
        }
    }
}
